mod builtin_words;
mod logic;
mod state;

use console;
use logic::*;
use rand::seq::SliceRandom;
use rand::{self, SeedableRng};
use rand_chacha;
use serde_json::Value;
use state::{Game, Stat, State};
use std::collections::HashSet;
use std::fs;
use std::io::{self};

// read a line from input
fn read_line() -> String {
    let mut line: String = String::new();
    match io::stdin().read_line(&mut line) {
        Ok(size) => {
            if size == 0 {
                panic!("ERROR: EOF IS READ FROM INPUT");
            }
        }
        Err(_) => {
            panic!("ERROR: FAILED TO READ LINE FROM INPUT");
        }
    }
    return line.trim().to_string();
}

// message: input is invalid
fn alert_invalid(is_tty: bool) {
    if is_tty {
        print!("Guess is invalid. Please make another guess.\n");
    } else {
        print!("INVALID\n");
    }
}

// message: answer is invalid
fn alert_answer_invalid(is_tty: bool) {
    if is_tty {
        print!("Answer is invalid. Please input another answer.\n");
    } else {
        print!("INVALID\n");
    }
}

// read a valid uppercase answer from input
fn read_ans(is_tty: bool, valid_set: &HashSet<&str>) -> String {
    if is_tty {
        print!("Please input the answer:\n");
    }
    let mut word: String = read_line();
    while !is_valid(&word, valid_set) {
        alert_answer_invalid(is_tty);
        word = read_line();
    }
    return word.to_ascii_uppercase();
}

// read a valid uppercase word from input
fn read_word(is_tty: bool, valid_set: &HashSet<&str>) -> String {
    if is_tty {
        print!("Please input your guess:\n");
    }
    let mut word: String = read_line();
    while !is_valid(&word, valid_set) {
        alert_invalid(is_tty);
        word = read_line();
    }
    return word.to_ascii_uppercase();
}

// read a valid uppercase word from input, satisfying difficult mode requirements
fn read_word_difficult_mode(
    is_tty: bool,
    valid_set: &HashSet<&str>,
    correct: &Vec<(char, usize)>,
    misplaced: &Vec<char>,
) -> String {
    if is_tty {
        print!("Please input your guess(it should satisfy hard mode requirements):\n");
    }
    let mut word: String = read_line();
    while !(is_valid(&word, valid_set)
        && check_difficult_mode(&word.to_ascii_uppercase(), correct, misplaced))
    {
        alert_invalid(is_tty);
        word = read_line();
    }
    return word.to_ascii_uppercase();
}

// message: you win
fn alert_win(is_tty: bool, tries: usize) {
    if is_tty {
        print!(
            "{}",
            console::style("WIN! Tries: ".to_string() + &tries.to_string())
                .green()
                .bold()
        );
        print!("\n");
    } else {
        print!("CORRECT {}\n", tries);
    }
}

// message: you lose
fn alert_lose(is_tty: bool, ans: &String) {
    if is_tty {
        print!(
            "{}",
            console::style("LOSE... Answer: ".to_string() + ans)
                .red()
                .bold()
        );
        print!("\n");
    } else {
        print!("FAILED {}\n", ans.to_ascii_uppercase());
    }
}

// print letter result
fn print_letter_result(is_tty: bool, letter_result: LetterResult) {
    if is_tty {
        match letter_result {
            LetterResult::Unknown => print!("{}", console::style("X")),
            LetterResult::TooMany => print!("{}", console::style("R").red()),
            LetterResult::Misplaced => print!("{}", console::style("Y").yellow().bold()),
            LetterResult::Correct => print!("{}", console::style("G").green().bold()),
        }
    } else {
        match letter_result {
            LetterResult::Unknown => print!("X"),
            LetterResult::TooMany => print!("R"),
            LetterResult::Misplaced => print!("Y"),
            LetterResult::Correct => print!("G"),
        }
    }
}

// print word result
fn print_word_result(is_tty: bool, word_results: &WordResult) {
    for x in word_results {
        print_letter_result(is_tty, *x);
    }
}

// print alphabet result
fn print_alphabet_result(is_tty: bool, alphabet_results: &AlphabetResult) {
    for x in alphabet_results {
        print_letter_result(is_tty, *x);
    }
}

// print guess result
fn print_result(is_tty: bool, word_results: &WordResult, alphabet_results: &AlphabetResult) {
    print_word_result(is_tty, word_results);
    print!(" ");
    print_alphabet_result(is_tty, alphabet_results);
    print!("\n");
}

// print statistics
fn print_stat(is_tty: bool, stat: &Stat) {
    let average_attempt: f64 = stat.average_attempt();
    if is_tty {
        print!("+-----------------+\n");
        print!("|   STATISTICS    |\n");
        print!("|                 |\n");
        print!("|Win        : {:^4}|\n", stat.win());
        print!("|Lose       : {:^4}|\n", stat.lose());
        if !average_attempt.is_nan() {
            print!("|Avg Attempt: {:^4.2}|\n", average_attempt);
        }
        print!("|                 |\n");
        print!("| FAVOURITE WORDS |\n");
        print!("|                 |\n");
        print!("|#RANK GUESS COUNT|\n");
        for (id, (guess, cnt)) in stat.most_frequent_words().iter().enumerate() {
            print!("|{:^5} {:^5} {:^5}|\n", id + 1, guess, cnt);
        }
        print!("+-----------------+\n");
    } else {
        print!(
            "{} {} {:.2}\n",
            stat.win(),
            stat.lose(),
            if average_attempt.is_nan() {
                0.0
            } else {
                average_attempt
            }
        );
        let mut first: bool = true;
        for (guess, cnt) in stat.most_frequent_words() {
            if first {
                first = false;
            } else {
                print!(" ");
            }
            print!("{} {}", guess, cnt);
        }
        print!("\n");
    }
}

// message: want to play another game?
fn alert_another_game(is_tty: bool) {
    if is_tty {
        print!("Do you want to play another game? (Y / n)\n");
    }
}

// state of argument parser
enum ArgumentParserState {
    Default,
    Answer,
    Seed,
    Day,
    Final,
    Acceptable,
    State,
    Config,
}

// the main function for the Wordle game
fn main() -> Result<(), Box<dyn std::error::Error>> {
    // judge if program is running in a is_tty
    let is_tty: bool = atty::is(atty::Stream::Stdout);
    if is_tty {
        print!("WORDLE GAME (CLI edition) by guyc22\n");
        print!("TTY environment detected\n");
    }

    // state of argument parser
    let mut argument_parser_state: ArgumentParserState = ArgumentParserState::Default;

    let mut read_answer_mode: bool = false;
    let mut random_mode: bool = false;
    let mut difficult_mode: bool = false;
    let mut stat_mode: bool = false;
    let mut seed_mode: bool = false;
    let mut day_mode: bool = false;
    let mut final_mode: bool = false;
    let mut acceptable_mode: bool = false;
    let mut state_mode: bool = false;
    let mut config_mode: bool = false;

    let mut ans: String = String::new();
    let mut final_filename: String = String::new();
    let mut acceptable_filename: String = String::new();
    let mut state_filename: String = String::new();
    let mut config_filename: String = String::new();
    let mut seed: u64 = 2022012761;
    let mut day: usize = 1;

    // parse the arguments in `args`
    for arg in std::env::args() {
        match argument_parser_state {
            ArgumentParserState::Default => match &arg as &str {
                "-w" | "--word" => {
                    read_answer_mode = true;
                    argument_parser_state = ArgumentParserState::Answer;
                }
                "-r" | "--random" => {
                    random_mode = true;
                }
                "-D" | "--difficult" => {
                    difficult_mode = true;
                }
                "-t" | "--stats" => {
                    stat_mode = true;
                }
                "-s" | "--seed" => {
                    seed_mode = true;
                    argument_parser_state = ArgumentParserState::Seed;
                }
                "-d" | "--day" => {
                    day_mode = true;
                    argument_parser_state = ArgumentParserState::Day;
                }
                "-f" | "--final-set" => {
                    final_mode = true;
                    argument_parser_state = ArgumentParserState::Final;
                }
                "-a" | "--acceptable-set" => {
                    acceptable_mode = true;
                    argument_parser_state = ArgumentParserState::Acceptable;
                }
                "-S" | "--state" => {
                    state_mode = true;
                    argument_parser_state = ArgumentParserState::State;
                }
                "-c" | "--config" => {
                    config_mode = true;
                    argument_parser_state = ArgumentParserState::Config;
                }
                _ => {}
            },
            ArgumentParserState::Answer => {
                ans = arg;
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::Seed => {
                seed = arg.parse().expect("ERROR: INVALID SEED ARGUMENT");
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::Day => {
                day = arg.parse().expect("ERROR: INVALID DAY ARGUMENT");
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::Final => {
                final_filename = arg;
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::Acceptable => {
                acceptable_filename = arg;
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::State => {
                state_filename = arg;
                argument_parser_state = ArgumentParserState::Default;
            }
            ArgumentParserState::Config => {
                config_filename = arg;
                argument_parser_state = ArgumentParserState::Default;
            }
        }
    }

    // check if an argument is missing
    match argument_parser_state {
        ArgumentParserState::Default => {}
        _ => panic!("ERROR: MISSING ARGUMENT"),
    }

    // read config from file
    if config_mode {
        let config: String =
            fs::read_to_string(config_filename).expect("ERROR: UNABLE TO READ CONFIG FILE");
        if let Value::Object(map) =
            serde_json::from_str(&config).expect("ERROR: FAILED TO PARSE CONFIG JSON")
        {
            for (key, value) in map {
                match &key as &str {
                    "word" => {
                        if let Value::String(str) = value {
                            if !read_answer_mode {
                                read_answer_mode = true;
                                ans = str;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: WORD)");
                        }
                    }
                    "random" => {
                        if let Value::Bool(b) = value {
                            if b {
                                random_mode = true;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: RANDOM)");
                        }
                    }
                    "difficult" => {
                        if let Value::Bool(b) = value {
                            if b {
                                difficult_mode = true;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: DIFFICULT)");
                        }
                    }
                    "stats" => {
                        if let Value::Bool(b) = value {
                            if b {
                                stat_mode = true;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: STATS)");
                        }
                    }
                    "seed" => {
                        if let Value::Number(number) = value {
                            if let Some(num) = number.as_u64() {
                                if !seed_mode {
                                    seed_mode = true;
                                    seed = num;
                                }
                            } else {
                                panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: SEED)");
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: SEED)");
                        }
                    }
                    "day" => {
                        if let Value::Number(number) = value {
                            if let Some(num) = number.as_u64() {
                                if !day_mode {
                                    day_mode = true;
                                    day = num as usize;
                                }
                            } else {
                                panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: DAY)");
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: DAY)");
                        }
                    }
                    "final_set" => {
                        if let Value::String(str) = value {
                            if !final_mode {
                                final_mode = true;
                                final_filename = str;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: FINAL_SET)");
                        }
                    }
                    "acceptable_set" => {
                        if let Value::String(str) = value {
                            if !acceptable_mode {
                                acceptable_mode = true;
                                acceptable_filename = str;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: ACCEPTABLE_SET)");
                        }
                    }
                    "state" => {
                        if let Value::String(str) = value {
                            if !state_mode {
                                state_mode = true;
                                state_filename = str;
                            }
                        } else {
                            panic!("ERROR: INVALID CONFIG ITEM VALUE(KEY: STATE)");
                        }
                    }
                    _ => {
                        panic!("ERROR: UNKNOWN CONFIG ITEM KEY");
                    }
                }
            }
        }
    }

    // check conflicting arguments
    if read_answer_mode && (random_mode || seed_mode || day_mode) {
        panic!("ERROR: READ ANSWER MODE AND RANDOM MODE CONFLICT");
    }

    // read final list from file & sort final list
    let final_content: String;
    let mut final_list = if final_mode {
        final_content =
            fs::read_to_string(final_filename).expect("ERROR: UNABLE TO READ FINAL SET FILE");
        final_content
            .split_ascii_whitespace()
            .map(|str| str.clone())
            .collect()
    } else {
        builtin_words::FINAL.to_vec()
    };
    final_list.sort_unstable();

    // read acceptable list from file & sort acceptable list
    let acceptable_content: String;
    let mut acceptable_list = if acceptable_mode {
        acceptable_content = fs::read_to_string(acceptable_filename)
            .expect("ERROR: UNABLE TO READ ACCEPTABLE SET FILE");
        acceptable_content
            .split_ascii_whitespace()
            .map(|str| str.clone())
            .collect()
    } else {
        builtin_words::ACCEPTABLE.to_vec()
    };
    acceptable_list.sort_unstable();

    // build HashSet and check subset constraint
    let final_set = build_set_from_list(&final_list);
    let acceptable_set = build_set_from_list(&acceptable_list);

    if !final_set.is_subset(&acceptable_set) {
        panic!("ERROR: FINAL SET IS NOT A SUBSET OF ACCEPTABLE SET");
    }

    // check answer validity & convert to uppercase
    if read_answer_mode {
        if !final_set.contains(&ans as &str) {
            panic!("ERROR: THE GIVEN ANSWER IS NOT IN THE FINAL SET");
        }
        ans = ans.to_ascii_uppercase();
    }

    // handle random seed
    if seed_mode {
        if day == 0 || day > final_list.len() {
            panic!("ERROR: DAY ARGUMENT OUT OF BOUND");
        }
        day -= 1;
        final_list.shuffle(&mut rand_chacha::ChaCha12Rng::seed_from_u64(seed));
    }

    // init state and statistics
    let state_content: String;
    let mut state: State = State::new();

    let mut stat: Stat = if state_mode {
        match fs::read_to_string(&state_filename) {
            Ok(content) => {
                // parse state file
                state_content = content;
                state = State::parse(&state_content);
                Stat::load_state(&state)
            }
            Err(_) => Stat::new(),
        }
    } else {
        Stat::new()
    };

    // game loop
    loop {
        let mut game_win: bool = false;
        let mut alphabet_results: AlphabetResult = [LetterResult::Unknown; ALPHABET_SIZE];

        // generate answer word if needed
        if !read_answer_mode {
            if random_mode {
                // randomly generate answer word
                if seed_mode {
                    // generate using given random seed and day
                    ans = final_list[day].to_ascii_uppercase();
                } else {
                    // generate freely~
                    ans = final_list
                        .choose(&mut rand::thread_rng())
                        .expect("ERROR: FAIL TO GENERATE ANSWER WORD")
                        .to_ascii_uppercase();
                }
            } else {
                // read answer word from input
                ans = read_ans(is_tty, &final_set);
            }
        }

        let ans_data: WordData = get_letters(&ans);

        let mut game: Game = Game::from_answer(ans.clone());

        let mut correct: Vec<(char, usize)> = Vec::new();
        let mut misplaced: Vec<char> = Vec::new();

        // tries
        for i in 1..=MAX_TRY {
            // read guess from input
            let guess: String = if difficult_mode {
                // check difficult mode requirements
                read_word_difficult_mode(is_tty, &acceptable_set, &correct, &misplaced)
            } else {
                read_word(is_tty, &acceptable_set)
            };

            // add guess to game and statistics
            game.add_guess(guess.clone());
            stat.add_guess(guess.clone());

            // calculate the result of guess
            let guess_data: WordData = get_letters(&guess);
            let word_results: WordResult =
                calculate_result(&mut alphabet_results, &ans_data, &guess_data);
            print_result(is_tty, &word_results, &alphabet_results);

            // if guess is correct, show message and exit loop
            if guess == ans {
                stat.add_win(i);
                alert_win(is_tty, i);
                game_win = true;
                break;
            }

            // update difficult mode requirements
            if difficult_mode {
                update_difficult_mode(&guess, &word_results, &mut correct, &mut misplaced)
            }
        }

        // oh no the game is lost
        if !game_win {
            stat.add_lose();
            alert_lose(is_tty, &ans);
        }

        // in read answer mode, there should be only 1 game
        if read_answer_mode {
            break;
        }

        // save game state if needed
        if state_mode {
            state.add_game(game);
            if fs::write(&state_filename, state.stringify()).is_err() {
                if is_tty {
                    print!("WARNING: Failed to save game state.\n");
                }
            }
        }

        // print statistics if needed
        if stat_mode {
            print_stat(is_tty, &stat);
        }

        // ask if another game should be played
        alert_another_game(is_tty);
        let mut line: String = String::new();
        match io::stdin().read_line(&mut line) {
            Ok(size) => {
                if size == 0 || line.trim() != "Y" {
                    break;
                }
            }
            Err(_) => {
                panic!("ERROR: FAILED TO READ LINE FROM INPUT");
            }
        }

        // a new day
        day += 1;
    }

    Ok(())
}
